/*
 * CCVisu is a tool for visual graph clustering
 * and general force-directed graph layout.
 * This file is part of CCVisu.
 *
 * Copyright (C) 2005-2012  Dirk Beyer
 *
 * CCVisu is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * CCVisu is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with CCVisu; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please find the GNU Lesser General Public License in file
 * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt
 *
 * Dirk Beyer    (firstname.lastname@uni-passau.de)
 * University of Passau, Bavaria, Germany
 */
package org.sosy_lab.ccvisu;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.sosy_lab.ccvisu.Options.InFormat;
import org.sosy_lab.ccvisu.Options.OptionsEnum;
import org.sosy_lab.ccvisu.Options.OutFormat;
import org.sosy_lab.ccvisu.clustering.ClustererMinDistPerc;
import org.sosy_lab.ccvisu.clustering.interfaces.Clusterer;
import org.sosy_lab.ccvisu.graph.GraphData;
import org.sosy_lab.ccvisu.graph.GraphVertex;
import org.sosy_lab.ccvisu.graph.NameVisibility;
import org.sosy_lab.ccvisu.graph.Position;
import org.sosy_lab.ccvisu.layout.Minimizer;
import org.sosy_lab.ccvisu.layout.MinimizerBarnesHut;
import org.sosy_lab.ccvisu.measuring.ClusterQuality;
import org.sosy_lab.ccvisu.measuring.calldep.DependencyCalculator;
import org.sosy_lab.ccvisu.readers.ReaderData;
import org.sosy_lab.ccvisu.readers.ReaderDataGraph;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphAUX;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphCVS;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphDOX;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphFilter;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphRSF;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphSoftware;
import org.sosy_lab.ccvisu.readers.ReaderDataGraphXML;
import org.sosy_lab.ccvisu.readers.ReaderDataGroup;
import org.sosy_lab.ccvisu.readers.ReaderDataLayoutLAY;
import org.sosy_lab.ccvisu.readers.filter.WhitelistRelationFilter;
import org.sosy_lab.ccvisu.ui.FrameDisplay;
import org.sosy_lab.ccvisu.ui.interfaces.GraphLoadedListener;
import org.sosy_lab.ccvisu.writers.Writer;
import org.sosy_lab.ccvisu.writers.WriterDataCalldep;
import org.sosy_lab.ccvisu.writers.WriterDataClustersRSF;
import org.sosy_lab.ccvisu.writers.WriterDataDistHist;
import org.sosy_lab.ccvisu.writers.WriterDataGraphGML;
import org.sosy_lab.ccvisu.writers.WriterDataGraphGXL;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutLAY;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutSVG;
import org.sosy_lab.ccvisu.writers.WriterDataLayoutVRML;
import org.sosy_lab.ccvisu.writers.WriterDataRSF;
import org.sosy_lab.ccvisu.writers.WriterDataSTATS;
import org.sosy_lab.ccvisu.writers.WriterGui;
import org.sosy_lab.ccvisu.writers.WriterNull;

public class CCVisuController {

  /** End of line.*/
  public final static String endl = System.getProperty("line.separator");

  private BufferedReader createInputReader(String inputFileName) throws IOException {
    // Setup of input reader.
    BufferedReader in = null;
    try {
      InputStream inStream = null;

      if (inputFileName.equalsIgnoreCase("stdin")) {
        inStream = System.in;
      } else {
        inStream = new FileInputStream(inputFileName);
      }

      if (inputFileName.endsWith(InFormat.ODS.getFileExtension())) {
        ZipInputStream zipIn = new ZipInputStream(inStream);
        // Look for file entry 'content.xml' in the ZIP file.
        ZipEntry entry = zipIn.getNextEntry();
        while (entry != null && !entry.getName().equals("content.xml")) {
          entry = zipIn.getNextEntry();
        }
        if (entry == null) {
          System.err.println("I/O Error reading 'content.xml' from ODS file.");
        }
        inStream = zipIn;
      }

      in = new BufferedReader(new InputStreamReader(inStream));
    } catch (IOException e) {
      throw new IOException("Exception while opening file '" + inputFileName + "' for reading.", e);
    }

    return in;
  }

  private void copyPositionsAndProperties(Options options) {
    if (options.initialLayout != null) {
      for (GraphVertex currVertex : options.graph.getVertices()) {
        GraphVertex oldVertex = options.initialLayout.getVertexByName(currVertex.getName());
        if (oldVertex != null) {
          currVertex.setColor(oldVertex.getColor());
          currVertex.setShowName(oldVertex);
          currVertex.setPosition(new Position(oldVertex.getPosition()));
        }
      }
    }
  }

  private void loadOrCalcInitialLayout(Options options) throws IOException {
    String layoutFileName = options.getOption(OptionsEnum.initLayout).getString();

    if (layoutFileName.trim().length() > 0) {
      BufferedReader initialLayoutStream = null;
      GraphData layout = options.initialLayout = new GraphData();

      // Read initial (pre-computed) layout from file.
      try {
        initialLayoutStream = new BufferedReader(new FileReader(layoutFileName));
        (new ReaderDataLayoutLAY(initialLayoutStream, options.verbosity)).read(layout);
      } catch (IOException e) {
        throw new IOException("Exception while opening file '" + layoutFileName + "' for reading.");
      }

      options.infoCollector.addMessage("Successfully loaded layout from file "
          + layoutFileName + " with " + layout.getVertices().size() + " vertices.");

      // Close the input file.
      try {
        initialLayoutStream.close();
      } catch (Exception e) {
        System.err.println("Exception while closing input file: ");
        System.err.println(e);
      }

      // Reset vertex degrees,
      // i.e., use the degrees from the graph and ignore the degree from read layout.
      for (GraphVertex itVertex : layout.getVertices()) {
        itVertex.setDegree(0);
      }
    }

    applyInitialLayout(options);
  }

  private void applyInitialLayout(Options options) {
    // vertex.fixedInitPos == true means that the minimizer does not change
    //   vertex's position.
    GraphData initialLayout = options.initialLayout;

    if (options.getOption(OptionsEnum.fixedInitPos).getBool() && initialLayout != null) {
      for (GraphVertex currVertex : options.graph.getVertices()) {
        // If the current vertex exists in the read initial layout,
        // then mark its position as fixed.
        if (initialLayout.getVertexByName(currVertex.getName()) != null) {
          currVertex.setFixedPos(true);
        }
      }
    }

    if (initialLayout == options.graph) {
      return;
    }

    if ((options.inFormat != InFormat.LAY)) {
      // Initialize with random positions.
      options.graph.setNodesToRandomPositions(options.graph.getVertices(), options.getOption(OptionsEnum.dim).getInt());
    }

    // Copy positions and properties from the initial layout
    //   that was read from file.
    copyPositionsAndProperties(options);
  }

  private void readGraph(Options options) throws IOException {
    String inputFileName = options.getOption(OptionsEnum.inputName).getString();
    BufferedReader in = createInputReader(inputFileName);
    try {
      ReaderData graphReader = getReader(options, inputFileName, in);

      // Set filter for selection of user relations for visualization.
      if (graphReader instanceof ReaderDataGraph) {
        if (options.filters.size() > 0) {
          // The user has given relations to select.
          graphReader =
              new ReaderDataGraphFilter((ReaderDataGraph) graphReader,
                                        options.verbosity,
                                        options.filters);
        } else {
          if (options.inFormat == InFormat.DOX && options.outFormat != OutFormat.RSF) {
            // The user has not given any relation name to select,
            //   but wants to actually draw the relation.
            System.err.println("Warning: Using default DOX-derived relation refFile.");
            System.err.println("Change this with parameter '-relSelect <rel-name>'");

            options.filters.add(new WhitelistRelationFilter("refFile"));
            graphReader =
                new ReaderDataGraphFilter((ReaderDataGraph) graphReader,
                                          options.verbosity,
                                          options.filters);
          }
        }
      }

      // Read the data using the reader (i.e., fill into existing graph structure).
      graphReader.read(options.graph);
      options.infoCollector.addMessage("Successfully loaded graph with "
          + options.graph.getVertices().size() + " vertices and "
          + options.graph.getEdges().size() + " edges.");

      // Handle vertex options.
      if (options.getOption(OptionsEnum.showAllLabels).getBool()) {
        for (GraphVertex curVertex : options.graph.getVertices()) {
          // showAllLabels (annotate each vertex with its name).
          curVertex.setShowName(NameVisibility.Priority.STARTUP, true);
        }
      }

    } finally {
      in.close();
    }
  }

  private ReaderData getReader(Options options, String inputFileName,
                               BufferedReader in) throws IOException {
    switch (options.inFormat) {
    case CVS:
      return new ReaderDataGraphCVS(in, options.verbosity,
          options.getOption(OptionsEnum.timeWindow).getInt(),
          options.getOption(OptionsEnum.slidingTW).getBool());
    case SVN:
      return new ReaderDataGraphXML(in, options.verbosity, options.inFormat);
    case DOX:
      return new ReaderDataGraphDOX(in, options.verbosity, inputFileName);
    case ODS:
      return new ReaderDataGraphXML(in, options.verbosity, options.inFormat);
    case RSF:
      return new ReaderDataGraphRSF(in, options.verbosity);
    case SRC:
      return new ReaderDataGraphSoftware(in, options.verbosity,
          options.getOption(OptionsEnum.sourceDirectory).getString(),
          options.getOption(OptionsEnum.sourceLibraries).getString());
    case AUX:
      // Graph is set by calling client. -- Do nothing.
      return  new ReaderDataGraphAUX(in, options.verbosity);
    case LAY:
      return new ReaderDataLayoutLAY(in, options.verbosity);
    default:
      throw new IOException("Runtime error: Unexpected input format '" + options.inFormat.toString() + "'.");
    }
  }

  public void process(Options options) throws IOException, InterruptedException {
    loadGraphAndInitGroups(options);

    // Run tools
    createAndRunMinimizer(options);
    runDepAnalysis(options);

    String initGroupsFileName = options.getOption(OptionsEnum.initGroups).getString();
    runClusteringAndWriteResults(options, initGroupsFileName);

    writeOutput(options.graph, options.getOption(OptionsEnum.outputName).getString(),
                options.outFormat, options);
  }

  private void loadGraphAndInitGroups(Options options) throws IOException {
    assert (options.graph != null);

    // Check for illegal option combinations...
    if (options.inFormat == InFormat.LAY && options.outFormat == OutFormat.RSF) {
      throw new IllegalArgumentException(
        "Usage error: Cannot produce RSF output from LAY input.");
    }

    readGraph(options);
    loadOrCalcInitialLayout(options);

    // Initialize groups.
    String initGroupsFileName = options.getOption(OptionsEnum.initGroups).getString();
    readInitialGroups(options, initGroupsFileName);

    // notify about newly loaded graph
    for (GraphLoadedListener listener : options.graphLoadedListeners) {
      listener.onGraphLoaded(options.graph);
    }
  }

  /**
   * Method to use Readers from the GUI.
   */
  public void processVisual(Options options, FrameDisplay display) throws IOException {
    loadGraphAndInitGroups(options);
  }

  private void runDepAnalysis(Options options) {
    if (options.getOption(OptionsEnum.cdep).getBool()) {
      DependencyCalculator dc = new DependencyCalculator(options.graph, options);
      dc.calculateAndSetDegree();
    }
  }

  private boolean isClusteringRequired(Options options) {
    int numberOfClusters = options.getOption(OptionsEnum.clusters).getInt();
    return (numberOfClusters > 0) || (options.outFormat == OutFormat.CRSF);
  }

  private void runClusteringAndWriteResults(Options options, String initGroupsFileName)
      throws InterruptedException {

    // Determine if we need to compute a clustering.
    if (isClusteringRequired(options)) {
      Clusterer clusterer =
          new ClustererMinDistPerc(options.graph, options.getOption(OptionsEnum.clusters).getInt(),
              options.getOption(OptionsEnum.clusterMergeDistancePercent).getFloat(),
                                options.getOption(OptionsEnum.clusterMinDistancePercent).getFloat());
      options.graph.clearGroups();
      clusterer.runClusteringAndUpdateGroups(options.graph);
    }

    // Calculate measures for clustering and append the results to the specified file.
    writeClusteringMeasures(options, initGroupsFileName,
        options.getOption(OptionsEnum.writeClusteringMeasures).getString());
  }

  protected void createAndRunMinimizer(Options options) {
    // Determine if we need to compute a layout.
    if ((options.inFormat != InFormat.LAY || options.outFormat == OutFormat.LAY) // only use minimizer if layout transformation is requested
     && options.outFormat != OutFormat.RSF // RSF output does not care about layout
     && options.outFormat != OutFormat.GML
     && options.outFormat != OutFormat.GXL
     ) {

      // Set minimizer algorithm.
      // So far there is only one implemented in CCVisu.
      Minimizer minimizer = new MinimizerBarnesHut(options);
      try {
        minimizer.minimizeEnergy();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public void writeOutput(GraphData graph, String outputFileName,
                          OutFormat outFormat, Options options)
                              throws IOException {

    PrintWriter printer = null;
    try {
      OutputStream outStream = null;
      if (outputFileName.equalsIgnoreCase("stdout")) {
        outStream = System.out;
      } else {
        outStream = new FileOutputStream(outputFileName);
      }
      printer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outStream)));
    } catch (IOException e) {
      throw new IOException("Exception while opening file '" + outputFileName + "' for writing.", e);
    }

    try {
      // Write the data using the writer.
      Writer writer = getWriter(outFormat, printer, options);
      writer.write();
      printer.flush();
    } finally {
      printer.close();
    }
  }

  private Writer getWriter(OutFormat outFormat, PrintWriter printer, Options options) throws IOException {
    GraphData graph = options.graph;

    switch (outFormat) {
    case RSF: // Graph in RSF.
      return new WriterDataRSF(printer, graph);
    case CRSF: // Clustering in RSF.
      return new WriterDataClustersRSF(printer, graph);
    case GML: // Graph in GraphML format.
      return new WriterDataGraphGML(printer, graph);
    case GXL: // Graph in GXL format.
      return new WriterDataGraphGXL(printer, graph);
    case LAY: // Layout in text format LAY.
      return new WriterDataLayoutLAY(printer, graph, options);
    case VRML: // Layout in VRML format.
      return new WriterDataLayoutVRML(printer, graph, options);
    case SVG: // Layout in SVG format.
      return new WriterDataLayoutSVG(printer, graph, options);
    case STATS: // Statistics in text format LAY.
      return new WriterDataSTATS(printer, graph);
    case CALLDEP: // Nodewise information for the calldep algorithm
      return new WriterDataCalldep(printer, graph, options);
    case DIST: // Euclidian distances between all node-pairs.
        return new WriterDataDistHist(printer, graph);
    case DISP: // Graph in a Window.
      return new WriterGui(this, options);
    case NULL: // Give no output
      return new WriterNull();
    default:
      throw new IOException("Runtime error: Unexpected output format '"
          + outFormat.toString() + "'.");
    }
  }

  private void writeClusteringMeasures(Options options, String initGroupsFileName,
                                       String writeClusterMeasuresTo) {

    if ((writeClusterMeasuresTo != null) && (writeClusterMeasuresTo.length() > 0)) {
      try {
        String clusteringName = initGroupsFileName;
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = options.getOption(OptionsEnum.inputName).getString();
        }
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = options.getOption(OptionsEnum.initLayout).getString();
        }
        if ((clusteringName == null) || (clusteringName.length() == 0)) {
          clusteringName = "UNNAMED";
        }

        FileWriter fileWriter = new FileWriter(writeClusterMeasuresTo, true);
        fileWriter.append(String.format("%s\t%d\t%d\t%d\t%d\t%f\t%f\t%f\t%f\n", clusteringName,
            options.graph.getNumberOfClusters(), options.graph.getVertices().size(), options.graph.getEdges().size(),
            ClusterQuality.cutOfClustering(options.graph), ClusterQuality.modularizationQualityOfGraph(options.graph),
            ClusterQuality.edgeNormalizedCutOfClustering(options.graph),
            ClusterQuality.edgeNormalizedCutOfClusteringV2(options.graph),
            ClusterQuality.modularityOfClustering(options.graph)));
        fileWriter.flush();

      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  private void readInitialGroups(Options options, String initGroupsFileName) {
    if (!initGroupsFileName.equals("")) {
      try {
        ReaderData groupReader =
            new ReaderDataGroup(new BufferedReader(new FileReader(initGroupsFileName)),
                options.verbosity);
        groupReader.read(options.graph);
      } catch (FileNotFoundException e) {
        System.err.println("Error reading file -- file not found: "
            + options.getOption(OptionsEnum.initGroups).getString());
      }
    }
  }

}
